/* Written 8/12/93 by Max Hailperin <max@nic.gac.edu>, Math/CS department,
   Gustavus Adolphus College.  Public domain, no warranty.  Please share
   any improvements with me. 

   Note: this communications protocol is for use with Best Fortress UPSs. */

#import "daemon.h"
#import "UPSController.h"
#import <sys/ioctl.h>
#import <sys/fcntl.h>
#import <dpsclient/dpsclient.h>
#import <appkit/Application.h>
#import <stdio.h>
#import <stdlib.h>
#import <libc.h>
#import <time.h>
#import <ctype.h>
#import <syslog.h>

#ifndef POLLING_PERIOD
#define POLLING_PERIOD 60.0
#endif
#ifndef RETRY_PERIOD
#define RETRY_PERIOD 5.0
#endif
#ifndef RETRIES
#define RETRIES 3
#endif
#ifndef SHUTDOWN_MINUTES
#define SHUTDOWN_MINUTES 5
#endif
#ifndef SHUTDOWN_ALARM_MASK
#define SHUTDOWN_ALARM_MASK 0x292
#endif
#ifndef SHUTDOWN_DELAY
#define SHUTDOWN_DELAY 60
#endif
#ifndef POWERDOWN_DELAY
#define POWERDOWN_DELAY 120
#endif

#define BYTES_IN_F_STRING 40
#define HEX_DIGITS_PER_BYTE 2

#define ctrl(c) ((c)&0x1f)

static struct {
  int systemMode, inverterStatus, alarmStatus, ACInputVolts, ACOutputVolts,
  ACOutputDeciAmperes, ACLoadVA, batteryDeciVolts, deciHertz, minutes,
  ambientTemp, ROMVersion, time;
} status;

static DPSTimedEntry retryTE;
static int retries;

static int checksumOK(const unsigned char *fString){
  unsigned char sum=0;
  int i;
  for(i = 0; i < BYTES_IN_F_STRING; i++)
    sum += fString[i];
  return sum == 0;
}

static int asDecimal(const unsigned char *bytes, int count){
  int value=0;
  while(count--){
    value *= 100;
    value += 10*(*bytes/16) + *bytes % 16;
    bytes++;
  }
  return value;
}

static void describeTrouble(){
  if(status.inverterStatus)
    syslog(FACILITY|LOG_EMERG,
           "Running on battery power; backup time remaining: %d minutes.",
           status.minutes);
  if(status.alarmStatus & 0x1)
    syslog(FACILITY|LOG_EMERG, "UPS memory error");
  if(status.alarmStatus & 0x2)
    syslog(FACILITY|LOG_EMERG, "Check battery");
  if(status.alarmStatus & 0x4)
    syslog(FACILITY|LOG_EMERG, "Check inverter");
  if(status.alarmStatus & 0x8)
    syslog(FACILITY|LOG_EMERG, "Low runtime remaining");
  if(status.alarmStatus & 0x10)
    syslog(FACILITY|LOG_EMERG, "Low battery");
  if(status.alarmStatus & 0x20)
    syslog(FACILITY|LOG_EMERG, "Circuit breaker shutdown");
  if(status.alarmStatus & 0x40)
    syslog(FACILITY|LOG_EMERG, "Overload");
  if(status.alarmStatus & 0x80)
    syslog(FACILITY|LOG_EMERG, "High temperature");
  if(status.alarmStatus & 0x100)
    syslog(FACILITY|LOG_EMERG, "Site wiring fault");
  if(status.alarmStatus & 0x200)
    syslog(FACILITY|LOG_EMERG, "High battery");
}

static void doShutdown(FILE *ups){
  syslog(FACILITY|LOG_EMERG,
         "Warning!!!  System shutdown in %d seconds; please log off NOW!",
         SHUTDOWN_DELAY);
#ifndef DEBUG
  sleep(SHUTDOWN_DELAY);
  fprintf(ups, "off %d\r", POWERDOWN_DELAY);
  system("/usr/etc/halt -p");
#endif
}

static void handleTrouble(FILE *ups){
  /* should do the filtering of transients like checkups somehow? */
  describeTrouble();
  if((status.alarmStatus & SHUTDOWN_ALARM_MASK) ||
     (status.inverterStatus && status.minutes < SHUTDOWN_MINUTES))
    doShutdown(ups);
}

static void processFString(const unsigned char *fString, FILE *ups){
  if(!checksumOK(fString))
    syslog(FACILITY|LOG_CRIT, "Checksum error in UPS F string");
  else{
    if(retryTE){
      DPSRemoveTimedEntry(retryTE);
      retryTE = 0;
    }
    status.systemMode = fString[5];
    status.inverterStatus = fString[8];
    status.alarmStatus = fString[11]<<8 + fString[10];
    status.ACInputVolts = asDecimal(fString+12, 2);
    status.ACOutputVolts = asDecimal(fString+14, 2);
    status.ACOutputDeciAmperes = asDecimal(fString+18, 2);
    status.ACLoadVA = asDecimal(fString+20, 3);
    status.batteryDeciVolts = asDecimal(fString+25, 2);
    status.deciHertz = asDecimal(fString+27, 2)/10;
    status.minutes = asDecimal(fString+29, 2);
    status.ambientTemp = asDecimal(fString+31, 2);
    status.ROMVersion = asDecimal(fString+37, 2);
    status.time = time(0);
    if(status.inverterStatus || status.alarmStatus)
      handleTrouble(ups);
  }
}

static void reallyRequestFString(FILE *ups){
  putc('f', ups);
  putc('\r', ups);
}

static void retry(DPSTimedEntry retryTE, double now, void *ups){
#ifdef DEBUG
printf(">>>retrying\n");
#endif
  if(retries++ == RETRIES)
    syslog(FACILITY|LOG_CRIT, "Not succesfully communicating with UPS");
  putc(ctrl('q'), (FILE*)ups);
  reallyRequestFString((FILE*)ups);
}

static void requestFString(FILE *ups){
  if(!retryTE){
    retries = 0;
    retryTE= DPSAddTimedEntry(RETRY_PERIOD, retry, ups, NX_RUNMODALTHRESHOLD);
    reallyRequestFString(ups);
  }
}
  
static void handleInput(int fd, void* vfp){
  static unsigned char bytes[BYTES_IN_F_STRING];
  static char hexDigits[HEX_DIGITS_PER_BYTE+1];
  static unsigned char *bytePtr=bytes;
  static char *hexPtr=hexDigits;
  int c;
  FILE *fp = (FILE*)vfp;

  while((c = getc(fp)) != EOF){
#ifdef DEBUG
putchar(c);
#endif
    if(c == '\r'
       && bytePtr-bytes == BYTES_IN_F_STRING
       && hexPtr == hexDigits){
      processFString(bytes, fp);
      bytePtr = bytes;
    } else if(!isxdigit(c)){
      if(c == '[' || c == ']' || c == '{' || c == '}')
        requestFString(fp);
      bytePtr = bytes;
      hexPtr = hexDigits;
    } else{
      *hexPtr++ = c;
      if(hexPtr-hexDigits == HEX_DIGITS_PER_BYTE){
        if(bytePtr-bytes == BYTES_IN_F_STRING){
          syslog(FACILITY|LOG_CRIT, "Too many hex digits in a row from UPS");
          bytePtr = bytes;
        }
        *bytePtr++ = strtol(hexDigits, NULL, 16);
        hexPtr = hexDigits;
      }
    }
  }
  clearerr(fp);
}

static void poll(DPSTimedEntry pollTE, double now, void *ups){
  requestFString((FILE*)ups);
}

@implementation UPSController

-initOn: (const char *)tty
{
  struct sgttyb modes = {B1200, B1200, 0, 0, RAW};
  FILE *ups;
  if((ups = fopen(tty, "r+")) == NULL){
        syslog(FACILITY|LOG_CRIT, "Can't open alleged tty %s; exiting.", tty);
        exit(1);
  }
  setbuf(ups, NULL);
  ioctl(fileno(ups), TIOCSETP, &modes);
  fcntl(fileno(ups), F_SETFL, FNDELAY);
  DPSAddFD(fileno(ups), handleInput, ups, NX_MODALRESPTHRESHOLD);
  requestFString(ups);
  DPSAddTimedEntry(POLLING_PERIOD, poll, ups, NX_RUNMODALTHRESHOLD);
  return self;
}

-(int)systemMode:(int*)systemMode inverterStatus:(int*)inverterStatus
      alarmStatus:(int*)alarmStatus ACInputVolts:(int*)ACInputVolts
      ACOutputVolts:(int*)ACOutputVolts
      ACOutputDeciAmperes:(int*)ACOutputDeciAmperes ACLoadVA:(int*)ACLoadVA
      batteryDeciVolts:(int*)batteryDeciVolts deciHertz:(int*)deciHertz
      minutes:(int*)minutes ambientTemp:(int*)ambientTemp
      ROMVersion:(int*)ROMVersion time:(int*)time
{
  *systemMode = status.systemMode;
  *inverterStatus = status.inverterStatus;
  *alarmStatus = status.alarmStatus;
  *ACInputVolts = status.ACInputVolts;
  *ACOutputVolts = status.ACOutputVolts;
  *ACOutputDeciAmperes = status.ACOutputDeciAmperes;
  *ACLoadVA = status.ACLoadVA;
  *batteryDeciVolts = status.batteryDeciVolts;
  *deciHertz = status.deciHertz;
  *minutes = status.minutes;
  *ambientTemp = status.ambientTemp;
  *ROMVersion = status.ROMVersion;
  *time = status.time;
  return 0;
}
@end
